package org.masterview.user.client.data;

import org.masterview.user.client.validation.Validator;
import org.masterview.user.client.validation.ValidatorBuilder;
import org.masterview.user.client.ui.DateComparator;
import org.masterview.user.client.ui.NumberComparator;
import org.masterview.user.client.ui.TextComparator;
import org.masterview.user.client.SortConstants;

import java.util.*;

public class DataSourceImpl implements DataSource {
    public static final int DEFAULT_PAGE_SIZE = 12;

    protected int pageSize;
    protected int currentPageNumber;

    protected String propertyToSort;

    protected String sortType = SortConstants.NO_SORTING;
    private int currentPageCount;

    public String getPropertyToSort() {
        return propertyToSort;
    }

    public void setPropertyToSort(String propertyToSort) {
        this.propertyToSort = propertyToSort;
        refreshDisplayData();
    }

    public String getSortType() {
        return sortType;
    }

    public void setSortType(String sortType) {
        this.sortType = sortType;
        refreshDisplayData();
    }

    /**
     * <p>The list of dataSource listeners.</p>
     */
    protected List dataSourceListeners = new ArrayList();

    /**
     * <p>The list of filters.</p>
     */
    protected List filters = new ArrayList();

    /**
     * <p>Filtered, paged and sorted data - ready to be displayed.</p>
     */
    protected List displayData = new ArrayList();

    /**
     * <p>Original data that is not affected by filters, sorting and pagination.</p>
     */
    protected List initialData = new ArrayList();

    /**
     * <p>The mapper that can be used to get values of the bean's fields by
     * names of these fields (it's used instead of
     * reflection that is not supported in GWT JRE).</p>
     * <p/>
     * <p>It's generated by MasterViewGenerator
     * in compile-time based on the class of the beans that would be keeped in datasource.</p>
     */
    protected PropertyMapper propertyMapper;

    /**
     * <p>Adds a datasource listener to a list of listeners. Later it will be notifyed then
     * the data will change.<p>
     *
     * @param dataSourceListener a listener to add.
     */
    public void addDataSourceListener(DataSourceListener dataSourceListener) {
        if (!dataSourceListeners.contains(dataSourceListener)) {
            dataSourceListeners.add(dataSourceListener);
        }
    }

    /**
     * <p>Removes a datasource listener from the list of listeners.</p>
     *
     * @param dataSourceListener a listener to remove.
     */
    public void removeDataSourceListener(DataSourceListener dataSourceListener) {
        if (dataSourceListeners.contains(dataSourceListener)) {
            dataSourceListeners.remove(dataSourceListener);
        }
    }

    /**
     * Gets property mapper. The property mapper is the object that can be used
     * to get values of the bean's fields by names of these fields (it's used instead of
     * reflection that is not supported in GWT JRE).
     *
     * @return
     */
    public PropertyMapper getPropertyMapper() {
        return propertyMapper;
    }

    public void setPropertyMapper(PropertyMapper propertyMapper) {
        this.propertyMapper = propertyMapper;
    }

    public DataSourceImpl() {
        this.currentPageNumber = 0;
        this.pageSize = DEFAULT_PAGE_SIZE;
    }

    public DataSourceImpl(List data) {
        this.currentPageNumber = 0;
        this.pageSize = DEFAULT_PAGE_SIZE;

        setInitialData(data);
        /*setDisplayData(data);*/        
    }

    public void clearData() {
        initialData = null;
    }

    public void addFilter(Filter filter) {
        if (!filters.contains(filter)) {
            filters.add(filter);
        }

        refreshDisplayData();
    }

    public void removeFilter(Filter filter) {
        if (filters.contains(filter)) {
            filters.remove(filter);
        }

        refreshDisplayData();
    }

    public void clearFilters(Filter filter) {
        filters.clear();
        refreshDisplayData();
    }

    public void clearFiltersByProperty(String propertyName) {
        for (int i = 0; i < filters.size(); i++) {
            if (!(filters.get(i) instanceof Filter)) {
                continue;
            }

            Filter filter = (Filter) filters.get(i);

            if (filter.getPropertyName().equals(propertyName)) {
                filters.remove(filter);
            }
        }
        refreshDisplayData();
    }

    public void setInitialData(List initialData) {
        this.initialData = initialData;
        refreshDisplayData();
    }


    public void setDisplayData(List displayData) {
        this.displayData = displayData;
        notifyDataSourceListeners(displayData.size(), initialData.size());
    }

    public List getDisplayData() {
        return displayData;
    }

    protected void notifyDataSourceListeners(int readItemsCount, int allItemsCount) {
        for (int i = 0; i < dataSourceListeners.size(); i++) {
            if (dataSourceListeners.get(i) instanceof DataSourceListener) {
                DataSourceListener dataSourceListener = (DataSourceListener) dataSourceListeners.get(i);
                dataSourceListener.onDataChanged(readItemsCount, allItemsCount);
            }
        }
    }

    /**
     * Filters the data with a list of assigned filtes.
     *
     */
    protected List filterData(List dataToFilter) {
        List filteredData = new ArrayList();

        for (int i = 0; i < getInitialData().size(); i++) {
            filteredData.add(getInitialData().get(i));
        }

        for (int i = 0; i < filters.size(); i++) {
            Filter filter = (Filter) filters.get(i);
            Validator validator =
                    ValidatorBuilder.buildFromFilterExpression(filter.getExpression());

            List toBeRemovedObjects = new ArrayList();

            for (int j = 0; j < filteredData.size(); j++) {
                Object objectToTest = filteredData.get(j);
                Object propertyValueToTest = propertyMapper.getProperty(objectToTest,
                        filter.getPropertyName());

                if (!validator.isValid(propertyValueToTest)) {
                    toBeRemovedObjects.add(objectToTest);
                }
            }

            for (int j = 0; j < toBeRemovedObjects.size(); j++) {
                filteredData.remove(toBeRemovedObjects.get(j));
            }
        }

        return filteredData;
    }

    protected List sortData(List dataToSort) {
        if (sortType.equals(SortConstants.NO_SORTING)) {
            return dataToSort;
        }

        final Comparator comparator;
        Object propertyValue = propertyMapper.getProperty(initialData.get(0), getPropertyToSort());

        if (propertyValue instanceof Date) {
            comparator = new DateComparator();
        } else if ( (propertyValue instanceof Integer) ||
                (propertyValue instanceof Long) ||
                (propertyValue instanceof Byte) ||
                (propertyValue instanceof Double) ||
                (propertyValue instanceof Float)) {
            comparator = new NumberComparator();
        } else {
            comparator = new TextComparator();
        }

        Collections.sort(dataToSort, new Comparator()
        {
            public int compare(Object firstBean, Object secondBbean) {
                Object firstObject = propertyMapper.getProperty(firstBean, propertyToSort);
                Object secondObject = propertyMapper.getProperty(secondBbean, propertyToSort);
                return comparator.compare(firstObject, secondObject);
            }
        });

        if (sortType.equals(SortConstants.SORT_DESC)) {
            Collections.reverse(dataToSort);
        }

        return dataToSort;
    }

    protected List paginateData(List dataToPaginate) {
        int firstEntityNumber = getPageSize() * getCurrentPageNumber();

        if (firstEntityNumber >= dataToPaginate.size()) {
            /*throw new EmptyDataSourceException("");*/
        }

        int lastEntityNumber = firstEntityNumber + pageSize;
        if (lastEntityNumber >= dataToPaginate.size()) {
            lastEntityNumber = dataToPaginate.size();
        }
        if (lastEntityNumber == firstEntityNumber) {
            /*throw new EmptyDataSourceException("");*/
        }

        List resultData = new ArrayList();
        for (int i = firstEntityNumber; i < lastEntityNumber; i++ ) {
            resultData.add(dataToPaginate.get(i));
        }

        return resultData;
    }

    public int getPageCount() {
        return currentPageCount;
    }

    public void refreshDisplayData() {
        if (initialData.size() == 0) {
            return;
        }

        List dataToBeDisplayed = new ArrayList();

        for (int i = 0; i < getInitialData().size(); i++) {
            dataToBeDisplayed.add(getInitialData().get(i));
        }

        dataToBeDisplayed = filterData(dataToBeDisplayed);
        dataToBeDisplayed = sortData(dataToBeDisplayed);

        currentPageCount = dataToBeDisplayed.size() / getPageSize() + 1;
        adjustCurrentPage();

        dataToBeDisplayed = paginateData(dataToBeDisplayed);
        setDisplayData(dataToBeDisplayed);
    }

    public List getInitialData() {
        return initialData;
    }

    public boolean isEmpty() {
        return initialData.isEmpty();
    }

    public int getCurrentPageNumber() {
        return currentPageNumber;
    }

    public void setCurrentPageNumber(int currentPageNumber) {
        if (getInitialData() == null) {
            setCurrentPageNumber(getLastPageNumber());
        } else if (currentPageNumber > getLastPageNumber()) {
            setCurrentPageNumber(getLastPageNumber());
        } else {
            this.currentPageNumber = currentPageNumber;
        }

        refreshDisplayData();
    }

    private int getLastPageNumber() {
        int lastPageNumber = getInitialData().size() / getPageSize();
        return lastPageNumber;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
        refreshDisplayData();
    }

    public void toPreviousPage() {
        setCurrentPageNumber(getCurrentPageNumber() - 1);
        refreshDisplayData();
    }

    public void toNextPage() {
        setCurrentPageNumber(getCurrentPageNumber() + 1);
        refreshDisplayData();
    }

    public void toFirstPage() {
        setCurrentPageNumber(0);
        refreshDisplayData();
    }

    public void toLastPage() {
        setCurrentPageNumber(getPageCount() - 1);
        refreshDisplayData();
    }

    public void adjustCurrentPage() {
        if (getCurrentPageNumber() >= getPageCount()) {
            setCurrentPageNumber(getPageCount() - 1);
        }
    }
}